package books;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import requestshandler.History;
import systemconfig.SystemConfiguration;
import users.Person;
import util.Holiday;
import exceptions.BookAlreadyReservedException;
import exceptions.InvalidBookInfoException;
import exceptions.LoanLimitExceededException;
import exceptions.NoCopyAvailableException;
import exceptions.NoReservedCopyException;

/**
 * Knows how to reserve a book to a Person and add a Person to the book's notification list. Adds new loans.
 */
public class ReservationsHandler {

    private BooksHandler booksHome;

    // private UsersHome usersHome;

    private Manager manager;

    private History loansHome;

    private Map<BookInfo, BookRequestsInfo> requests; // esto deberia ser una home?

    private Map<Person, List<BookReservation>> reservationByPerson;

    public void setBooksHome(final BooksHandler booksHome) {
        this.booksHome = booksHome;
    }

    private Map<Book, BookReservation> reservedBooks;

    // ********************* CONSTRUCTORS ************************

    public ReservationsHandler(final BooksHandler aBooksHome, final History aLoansHome, final Manager aManager) {
        this.booksHome = aBooksHome;
        this.requests = new HashMap<BookInfo, BookRequestsInfo>();
        this.loansHome = aLoansHome;
        this.reservationByPerson = new HashMap<Person, List<BookReservation>>();
        this.manager = aManager;
        this.reservedBooks = new HashMap<Book, BookReservation>();
    }

    // ********************* ACCESORS *******************************

    public void setLoansHome(final History loansHome) {
        this.loansHome = loansHome;
    }

    public History getLoansHome() {
        return this.loansHome;
    }

    public void setReservationByPerson(final Map<Person, List<BookReservation>> reservationByPerson) {
        this.reservationByPerson = reservationByPerson;
    }

    public Map<Person, List<BookReservation>> getReservationByPerson() {
        return this.reservationByPerson;
    }

    public void setManager(final Manager manager) {
        this.manager = manager;
    }

    public Manager getManager() {
        return this.manager;
    }

    protected BooksHandler getBooksHome() {
        return this.booksHome;
    }

    public void setRequests(final Map<BookInfo, BookRequestsInfo> requests) {
        this.requests = requests;
    }

    protected Map<BookInfo, BookRequestsInfo> getRequests() {
        return this.requests;
    }

    public void setReservedBooks(final Map<Book, BookReservation> reservedBooks) {
        this.reservedBooks = reservedBooks;
    }

    public Map<Book, BookReservation> getReservedBooks() {
        return this.reservedBooks;
    }

    // ***************** OTHER BEHAVIOUR*****************************

    public void addToNotificationList(final Person aPerson, final BookInfo aBook) {
        this.validateGetBookInfo(aBook);
        this.validateEntry(aBook);
        this.getRequestsOf(aBook).addPersonToNotificationList(aPerson);
    }

    public void reserveTo(final BookReservation aReservation) {

        this.getRequestsOf(aReservation.getBook()).reserveTo(aReservation);
        this.getReservationByPerson().get(aReservation.getPerson()).add(aReservation);
    }

    public void clearReservation(final BookInfo aBookInfo) {
        this.validateGetBookInfo(aBookInfo);
        Person reserver = null;
        BookReservation reservation = null;
        if (this.getRequests().containsKey(aBookInfo)) {
            reservation = this.getRequestsOf(aBookInfo).getReservedTo();
            reserver = reservation.getPerson();
            this.getRequestsOf(aBookInfo).clearReservation();
        }
        if (this.getReservationByPerson().containsKey(reserver)) {
            this.getReservationsOf(reserver).remove(reservation);
        }
    }

    public boolean isReserved(final BookInfo aBookInfo) {
        return this.getRequestsOf(aBookInfo).isReserved();
    }

    public BookReservation getReservation(final BookInfo aBookInfo) {
        if (this.getRequests().containsKey(aBookInfo)) {
            return this.getRequestsOf(aBookInfo).getReservedTo();
        } else {
            throw new InvalidBookInfoException();
        }
    }

    protected BookRequestsInfo getRequestsOf(final BookInfo aBookInfo) {
        this.validateEntry(aBookInfo);
        return this.getRequests().get(aBookInfo);
    }

    protected void validateGetBookInfo(final BookInfo aBookInfo) {
        this.getBooksHome().validateGetBookInfo(aBookInfo);
    }

    protected void validateEntry(final BookInfo aBookInfo) {
        // si tengo un map, tengo que crear la entrada
        if (!this.getRequests().containsKey(aBookInfo)) {
            this.getRequests().put(aBookInfo, new BookRequestsInfo());
        }
    }

    public void clearNotificationList(final BookInfo aBookInfo) {
        this.validateGetBookInfo(aBookInfo);
        if (this.getRequests().containsKey(aBookInfo)) {
            this.getRequestsOf(aBookInfo).clearNotificationList();
        }

    }

    public List<Person> getNotificationList(final BookInfo aBookInfo) {
        this.validateGetBookInfo(aBookInfo);
        this.validateEntry(aBookInfo);

        return this.getRequestsOf(aBookInfo).getNotificationList();

    }

    public Book borrowCopy(final BookInfo info) {
        return this.getBooksHome().borrowACopy(info);
    }

    public List<Book> getAvailableCopies(final BookInfo aBookInfo) {
        List<Book> availableCopies = new ArrayList<Book>(this.getBooksHome().getAvailableCopies(aBookInfo));
        if (this.isReserved(aBookInfo) && !availableCopies.isEmpty()) {
            availableCopies.remove(0);
        }
        return availableCopies;
    }

    public List<Book> getBorrowedCopies(final BookInfo aBookInfo) {
        return this.getBooksHome().getBorrowedCopies(aBookInfo);
    }

    public int timesBorrowed(final BookInfo aBookInfo) {
        return this.getBooksHome().getTimesBorrowed(aBookInfo);
    }

    public void newLoan(final BookInfo aBookInfo, final Person aPerson) {
        if (this.hasReservationOn(aBookInfo, aPerson)) {
            this.newLoanFromReservation(aBookInfo, aPerson);
        } else {
            this.newCommonLoan(aBookInfo, aPerson);
        }
    }

    private boolean hasReservationOn(final BookInfo aBookInfo, final Person aPerson) {
        if (this.isReserved(aBookInfo)) {
            return this.getReservation(aBookInfo).getPerson().equals(aPerson);
        } else {
            return false;
        }
    }

    public void newCommonLoan(final BookInfo aBookInfo, final Person aPerson) {
        this.validateLoanFor(aPerson);
        Book copy = this.borrowNotReservedCopy(aBookInfo);

        DateTime loanEnd = this.getEndLoanDate();
        Loan newLoan = new Loan(loanEnd, aPerson, copy);
        this.getLoansHome().storeNewLoan(newLoan); // posible metodo abstracto?
    }

    private Book borrowNotReservedCopy(final BookInfo aBookInfo) {
        int amountOfCopies = this.getBooksHome().getAvailableCopies(aBookInfo).size();
        if (amountOfCopies < 2 && this.isReserved(aBookInfo)) {
            throw new NoCopyAvailableException();
        }
        return this.borrowCopy(aBookInfo);
    }

    public void newLoanFromReservation(final BookInfo aBookInfo, final Person aPerson) {
        Book copy = this.getReservedBook(aBookInfo);
        this.clearReservation(aBookInfo);

        DateTime loanEnd = this.getEndLoanDate();
        Loan newLoan = new Loan(loanEnd, aPerson, copy);
        this.getLoansHome().storeNewLoan(newLoan);
    }

    private Book getReservedBook(final BookInfo aBookInfo) {
        Book toReturn = null;
        for (Book copy : this.getReservedBooks().keySet()) {
            if (copy.getInfo().equals(aBookInfo)) {
                toReturn = copy;
            }
        }
        if (toReturn == null) {
            throw new NoReservedCopyException();
        }
        return toReturn;

    }

    private DateTime getEndLoanDate() {
        DateTime nextEnd = new DateTime(DateTimeZone.forID("America/Argentina/Buenos_Aires"))
                .plusDays(SystemConfiguration.getInstance().getMaxDaysInLoaning());// esto en el constructor de Loan?

        // una forma feliz de saber que no es feriado

        return nextEnd;
    }

    private void validateLoanFor(final Person aPerson) {
        this.validateReservationsAndReservesAmount(aPerson);
    }

    private void validateReservationsAndReservesAmount(final Person aPerson) {
        int currentLoans = this.getLoansHome().getCurrentLoans(aPerson).size();
        int currentReservations = this.getReservationsOf(aPerson).size();

        int limit = SystemConfiguration.getInstance().getMaxLoansPerUser();
        if (currentLoans + currentReservations >= limit) {
            throw new LoanLimitExceededException();
        }
    }

    public List<BookReservation> getReservationsOf(final Person aPerson) {
        return this.validatePerson(aPerson);
    }

    private List<BookReservation> validatePerson(final Person aPerson) {
        List<BookReservation> reservations;
        if (this.getReservationByPerson().containsKey(aPerson)) {
            reservations = this.getReservationByPerson().get(aPerson);
        } else {
            reservations = new ArrayList<BookReservation>();
            this.getReservationByPerson().put(aPerson, reservations);

        }
        return reservations;
    }

    public void returnLoan(final Loan loan) {
        // QUE EL RETURN SIEMPRE SEA UNA COPIA y se le avise a la history que finalice el loan
        this.getLoansHome().setCurrentLoanAsFinished(loan);
        this.returnBook(loan.getLoanedBook());
    }

    public void returnBook(final Book aCopy) {
        // this.getLoansHome().finishLoan(aCopy);
        BookInfo info = aCopy.getInfo();
        if (this.isReserved(info)) {
            BookReservation reservation = this.getReservation(info);
            this.setReservedBook(aCopy, reservation);
            // this.getManager(). avisar que volvio la copia al manager para que mande mail
            this.clearReservation(info);
        } else {
            this.getRequestsOf(aCopy.getInfo()).copyReturned();
            this.getBooksHome().returnCopy(aCopy);
            this.getManager().sendCopyAvailableMailTo(this.getNotificationList(info)); // no decirle directamente que
                                                                                       // mande mail
            this.clearNotificationList(info);
        }
    }

    public void reserveBook(final BookInfo aBookInfo, final Person aPerson) {
        this.validateReservationsAndReservesAmount(aPerson);
        this.validateEntry(aBookInfo);
        this.validatePerson(aPerson);
        this.validateBookNotReserved(aBookInfo);
        this.validateGetBookInfo(aBookInfo);

        BookReservation newReservation = new BookReservation(aBookInfo, aPerson);
        this.reserveTo(newReservation);

    }

    private void validateBookNotReserved(final BookInfo aBookInfo) {
        if (this.isReserved(aBookInfo)) {
            throw new BookAlreadyReservedException();
        }

    }

    public void cancelExpiredReservations() {
        Map<Book, BookReservation> reservations = this.getReservedBooks();
        Set<Entry<Book, BookReservation>> entries = reservations.entrySet();
        for (Entry<Book, BookReservation> entry : entries) {
            if (this.reservationOutOfDate(reservations.get(entry.getKey()))) {
                this.cancelCopyReservation(entry.getKey());
            }
        }
    }

    public boolean reservationOutOfDate(final BookReservation bookReservation) {
        DateTime copyReturned, expiresOn;
        copyReturned = bookReservation.getCopyReturnedDate();

        int daysOfReserve = SystemConfiguration.getInstance().getDaysCopyReserved();
        expiresOn = new Holiday(null, null).getNonHolidayDateFrom(copyReturned, daysOfReserve, SystemConfiguration
                .getInstance().getHolidayList());
        expiresOn = expiresOn.plusDays(1);

        return expiresOn.isBeforeNow();

    }

    public void setReservedBook(final Book aCopy, final BookReservation reservation) {
        reservation.copyReturned();
        this.getReservedBooks().put(aCopy, reservation);
    }

    public void cancelCopyReservation(final Book copy) {
        this.getReservedBooks().remove(copy);
        this.returnBook(copy);
    }
}
